/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.basics.manager.impl;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileCacheImageOutputStream;

import com.genia.toolbox.basics.bean.DataContainer;
import com.genia.toolbox.basics.bean.image.AddTextTransformation;
import com.genia.toolbox.basics.bean.image.ImageDataContainer;
import com.genia.toolbox.basics.bean.image.Point;
import com.genia.toolbox.basics.bean.impl.AbstractDataContainer;
import com.genia.toolbox.basics.exception.technical.TechnicalException;
import com.genia.toolbox.basics.exception.technical.WrongImplementationException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.FileManager;
import com.genia.toolbox.basics.manager.ImageManager;

/**
 * {@link ImageManager} using AWT for image manipulation.
 */
public class AwtImageManager
    implements ImageManager
{
  /**
   * the {@link FileManager} to use.
   */
  private FileManager fileManager;

  /**
   * implementation of {@link ImageDataContainer} used by this manager.
   */
  private class AwtImageDataContainer
      extends AbstractDataContainer
      implements ImageDataContainer
  {

    /**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object. A subclass
     * overrides the <code>finalize</code> method to dispose of system
     * resources or to perform other cleanup.
     * 
     * @throws Throwable
     *           the <code>Exception</code> raised by this method
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize()
        throws Throwable
    {
      try {
        resetCachedRenderedImage();
      }
      finally {
        super.finalize();
      }
    }

    /**
     * the {@link BufferedImage} this {@link ImageDataContainer} represents.
     */
    private BufferedImage bufferedImage = null;

    /**
     * a temporary {@link File} containing the datas of this
     * {@link ImageDataContainer}.
     */
    private transient File cachedRenderedImage = null;



    /**
     * returns a new {@link ImageDataContainer} containing the content of the
     * current {@link ImageDataContainer} with a surimposed text.
     * 
     * @param addTextTransformation
     *          the parameters of the surimposed text.
     * @return a new {@link ImageDataContainer} containing the content of the
     *         current {@link ImageDataContainer} with a surimposed text
     * @throws TechnicalException
     *           when an error occured
     * @see com.genia.toolbox.basics.bean.image.ImageDataContainer#addText(com.genia.toolbox.basics.bean.image.AddTextTransformation)
     */
    public AwtImageDataContainer addText(AddTextTransformation addTextTransformation)
        throws TechnicalException
    {
      AwtImageDataContainer res = copy();

      if (addTextTransformation == null) {
        return res;
      }
      String text = addTextTransformation.getText();
      if (text == null) {
        return res;
      }

      BufferedImage resultImage = res.getBufferedImage();
      Graphics2D graphics2D = resultImage.createGraphics();
      Font font = addTextTransformation.getFont();
      if (font == null) {
        font = graphics2D.getFont();
      }
      font = font.deriveFont(addTextTransformation.getFontSize());

      graphics2D.setFont(font);

      graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
      graphics2D.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
      graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);

      GlyphVector glyphVector = font.createGlyphVector(graphics2D.getFontRenderContext(), text);

      double angle = 0;
      if (addTextTransformation.getAngle() != null) {
        angle = -addTextTransformation.getAngle();
      }

      if (angle != 0) {
        AffineTransform tranformation = AffineTransform.getRotateInstance(angle);
        for (int i = 0; i < glyphVector.getNumGlyphs(); i++) {
          glyphVector.setGlyphTransform(i, tranformation);
        }
      }
      Rectangle2D bounds = glyphVector.getOutline().getBounds();

      Integer xCoordinate = addTextTransformation.getStartingX();
      if (xCoordinate == null) {
        xCoordinate = (int) (resultImage.getWidth() - bounds.getWidth()) / 2;
      }
      else if (xCoordinate < 0) {
        xCoordinate = (int) (resultImage.getWidth() - bounds.getWidth() + xCoordinate);
      }

      xCoordinate = (int) (xCoordinate - bounds.getMinX());

      Integer yCoordinate = addTextTransformation.getStartingY();
      if (yCoordinate == null) {
        yCoordinate = (int) (resultImage.getHeight() - bounds.getHeight()) / 2;
      }
      else if (yCoordinate < 0) {
        yCoordinate = (int) (resultImage.getHeight() - bounds.getHeight() + yCoordinate);
      }

      yCoordinate = (int) (yCoordinate - bounds.getMinY());

      Color color = addTextTransformation.getColor();
      if (color == null) {
        color = Color.BLACK;
      }

      graphics2D.setColor(color);

      graphics2D.drawGlyphVector(glyphVector, xCoordinate, yCoordinate);
      graphics2D.dispose();

      return res;
    }
    
    /**
     * returns a new {@link ImageDataContainer} containing the image copied on top.
     * 
     * @param image
     *          the parameters of the surimposed image.
     * @param point
     *          the coords where to copy image on top.
     * @return a new {@link ImageDataContainer} containing the content of the
     *         current {@link ImageDataContainer} with a surimposed image
     * @throws TechnicalException
     *           when an error occured
     */
    public AwtImageDataContainer copyImage(ImageDataContainer image, Point point)
        throws TechnicalException {
      
      if (!(image instanceof AwtImageDataContainer)) {
        throw new WrongImplementationException(image.getClass(), AwtImageDataContainer.class);
      }
      
      AwtImageDataContainer res = copy();
        
      Graphics2D graphics2D = res.getBufferedImage().createGraphics();
      
      graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
      
      graphics2D.drawImage(((AwtImageDataContainer) image).getBufferedImage(), point.getX(), 
          point.getY(), image.getWidth(), image.getHeight(), null);
      
      graphics2D.dispose();
        
      return res;
    }



    /**
     * returns a copy of the current {@link ImageDataContainer}.
     * 
     * @return a copy of the current {@link ImageDataContainer}
     * @throws TechnicalException
     *           when an error occured
     */
    private AwtImageDataContainer copy()
        throws TechnicalException
    {
      BufferedImage transformedImage = new BufferedImage(getBufferedImage().getWidth(), getBufferedImage().getHeight(), getBufferedImageType());
      Graphics2D graphics2D = transformedImage.createGraphics();
      graphics2D.drawImage(getBufferedImage(), 0, 0, null);
      graphics2D.dispose();

      AwtImageDataContainer result = new AwtImageDataContainer();
      result.setContentType(getContentType());
      result.setName(getName());
      result.setBufferedImage(transformedImage);
      return result;

    }



    /**
     * getter for the bufferedImage property.
     * 
     * @return the bufferedImage
     */
    private BufferedImage getBufferedImage()
    {
      return bufferedImage;
    }



    /**
     * returns the type of {@link BufferedImage} to create to be compatible with
     * the current {@link BufferedImage}.
     * 
     * @return the type of {@link BufferedImage} to create to be compatible with
     *         the current {@link BufferedImage}
     */
    private int getBufferedImageType()
    {
      int type = getBufferedImage().getType();
      if (type == 0) {
        type = BufferedImage.TYPE_INT_ARGB;
      }
      return type;
    }



    /**
     * returns the height of the image in pixel.
     * 
     * @return the height of the image in pixel
     * @see com.genia.toolbox.basics.bean.image.ImageDataContainer#getHeight()
     */
    public int getHeight()
    {
      return getBufferedImage().getHeight();
    }



    /**
     * returns the {@link InputStream} containing the file datas. Consecutive
     * call to this method must always return the same {@link InputStream}.
     * 
     * @return the {@link InputStream} containing the file datas
     * @throws TechnicalException
     *           when an error occured
     * @see com.genia.toolbox.basics.bean.DataContainer#getInputStream()
     */
    public InputStream getInputStream()
        throws TechnicalException
    {

      try {
        if (cachedRenderedImage == null || !cachedRenderedImage.exists()) {
          ImageWriter imageWriter = ImageIO.getImageWritersByMIMEType(getContentType()).next();
          cachedRenderedImage = getFileManager().createAutoDeletableTempFile("image", ".bin");
          FileCacheImageOutputStream outputStream = new FileCacheImageOutputStream(new FileOutputStream(cachedRenderedImage), null);
          imageWriter.setOutput(outputStream);
          imageWriter.write(getBufferedImage());
        }
        return new FileInputStream(cachedRenderedImage);
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * returns the width of the image in pixel.
     * 
     * @return the width of the image in pixel
     * @see com.genia.toolbox.basics.bean.image.ImageDataContainer#getWidth()
     */
    public int getWidth()
    {
      return getBufferedImage().getWidth();
    }



    /**
     * reset the {@link File} containing the data of this container. It is
     * called each time those datas must be recomputed.
     */
    private void resetCachedRenderedImage()
    {
      if (cachedRenderedImage != null) {
        cachedRenderedImage.delete();
        cachedRenderedImage = null;
      }
    }



    /**
     * retruns a new {@link ImageDataContainer} being the current
     * {@link ImageDataContainer} resized to the given parameters.
     * 
     * @param width
     *          the width of the new image
     * @param height
     *          the height of the new image
     * @return a new {@link ImageDataContainer} begin the current
     *         {@link ImageDataContainer} resized to the given parameters
     * @throws TechnicalException
     *           when an error occured
     * @see com.genia.toolbox.basics.bean.image.ImageDataContainer#resize(int,
     *      int)
     */
    public AwtImageDataContainer resize(int width, int height)
        throws TechnicalException
    {
      BufferedImage transformedImage = new BufferedImage(width, height, getBufferedImageType());
      Graphics2D graphics2D = transformedImage.createGraphics();
      graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
      double scalex = (double) transformedImage.getWidth() / getBufferedImage().getWidth();
      double scaley = (double) transformedImage.getHeight() / getBufferedImage().getHeight();
      AffineTransform affineTransform = AffineTransform.getScaleInstance(scalex, scaley);
      graphics2D.drawRenderedImage(getBufferedImage(), affineTransform);
      graphics2D.dispose();

      AwtImageDataContainer result = new AwtImageDataContainer();
      result.setContentType(getContentType());
      result.setName(getName());
      result.setBufferedImage(transformedImage);
      return result;
    }



    /**
     * retruns a new {@link ImageDataContainer} being the current
     * {@link ImageDataContainer} resized proportionnaly so that the height of
     * the image is the one given in parameter.
     * 
     * @param height
     *          the height of the new image
     * @return a new {@link ImageDataContainer} being the current
     *         {@link ImageDataContainer} resized proportionnaly so that the
     *         height of the image is the one given in parameter
     * @throws TechnicalException
     *           when an error occured
     * @see com.genia.toolbox.basics.bean.image.ImageDataContainer#resizeByHeight(int)
     */
    public AwtImageDataContainer resizeByHeight(int height)
        throws TechnicalException
    {
      int width = getBufferedImage().getWidth() * height / getBufferedImage().getHeight();
      return resize(width, height);
    }



    /**
     * retruns a new {@link ImageDataContainer} being the current
     * {@link ImageDataContainer} resized proportionnaly so that the width of
     * the image is the one given in parameter.
     * 
     * @param width
     *          the width of the new image
     * @return a new {@link ImageDataContainer} being the current
     *         {@link ImageDataContainer} resized proportionnaly so that the
     *         width of the image is the one given in parameter
     * @throws TechnicalException
     *           when an error occured
     * @see com.genia.toolbox.basics.bean.image.ImageDataContainer#resizeByWidth(int)
     */
    public AwtImageDataContainer resizeByWidth(int width)
        throws TechnicalException
    {
      int height = getBufferedImage().getHeight() * width / getBufferedImage().getWidth();
      return resize(width, height);
    }



    /**
     * returns a new {@link ImageDataContainer} being the current
     * {@link ImageDataContainer} resized so proportionnaly to fit in the box
     * size given in parameters.
     * 
     * @param width
     *          the maximal width of the new image
     * @param height
     *          the maximal height of the new image
     * @return a new {@link ImageDataContainer} being the current
     *         {@link ImageDataContainer} resized so proportionnaly to fit in
     *         the box size given in parameters
     * @throws TechnicalException
     *           when an error occured
     * @see com.genia.toolbox.basics.bean.image.ImageDataContainer#resizeInBox(int,
     *      int)
     */
    public AwtImageDataContainer resizeInBox(int width, int height)
        throws TechnicalException
    {
      if (getBufferedImage().getHeight() * width <= getBufferedImage().getWidth() * height) {
        return resizeByWidth(width);
      }
      return resizeByHeight(height);
    }



    /**
     * setter for the bufferedImage property.
     * 
     * @param bufferedImage
     *          the bufferedImage to set
     */
    private void setBufferedImage(BufferedImage bufferedImage)
    {
      resetCachedRenderedImage();
      this.bufferedImage = bufferedImage;
    }



    /**
     * setter for the contentType property. the content type must be of type
     * "image/?" or an exception will be thrown.
     * 
     * @param contentType
     *          the contentType to set
     * @throws TechnicalException
     *           when an error occured
     * @see com.genia.toolbox.basics.bean.impl.AbstractDataContainer#setContentType(java.lang.String)
     */
    @Override
    public void setContentType(String contentType)
        throws TechnicalException
    {
      if (contentType == null || !contentType.startsWith("image/")) {
        throw new TechnicalException("The content type is not an image.", NOT_AN_IMAGE_ERROT_KEY, contentType);
      }
      resetCachedRenderedImage();
      super.setContentType(contentType);
    }
  }

  /**
   * Error key for the exception used when a file that is not an image is used.
   */
  public static final String NOT_AN_IMAGE_ERROT_KEY = AwtImageManager.class.getName() + ".notAnImage";

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;



  /**
   * create an {@link ImageDataContainer} from a {@link DataContainer}
   * containing an image.
   * 
   * @param dataContainer
   *          the {@link DataContainer} containing an image
   * @return an {@link ImageDataContainer} containing the image
   * @throws TechnicalException
   *           when an error occured
   * @see com.genia.toolbox.basics.manager.ImageManager#createImageDataContainer(com.genia.toolbox.basics.bean.DataContainer)
   */
  public ImageDataContainer createImageDataContainer(DataContainer dataContainer)
      throws TechnicalException
  {
    try {
      AwtImageDataContainer result = new AwtImageDataContainer();
      result.setContentType(dataContainer.getContentType());
      result.setName(dataContainer.getName());
      result.setBufferedImage(ImageIO.read(dataContainer.getInputStream()));
      return result;
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * getter for the fileManager property.
   * 
   * @return the fileManager
   */
  public FileManager getFileManager()
  {
    return fileManager;
  }



  /**
   * setter for the fileManager property.
   * 
   * @param fileManager
   *          the fileManager to set
   */
  public void setFileManager(FileManager fileManager)
  {
    this.fileManager = fileManager;
  }
}